package serial

import (
	"errors"
	"fmt"
	"math"
	"strings"
	"sync"
	"time"

	serial "gitee.com/shipnet/xserial-go"
	"github.com/project-flogo/core/activity"
	"github.com/project-flogo/core/app"
	"github.com/project-flogo/core/data/metadata"
)

var activityMd = activity.ToMetadata(&Settings{}, &Input{}, &Output{})

func init() {
	_ = activity.Register(&Activity{}, New)
}

type Activity struct {
	settings Settings
	serial   *serial.Serial
}

func New(ctx activity.InitContext) (activity.Activity, error) {
	settings := Settings{}
	err := metadata.MapToStruct(ctx.Settings(), &settings, true)
	if err != nil {
		return nil, err
	}
	locker, err := getOrCreateGlobalLock()
	if err != nil {
		return nil, err
	}
	stopBits, err := toStopBits(settings.StopBits)
	if err != nil {
		return nil, err
	}
	parity, err := toParity(settings.Parity)
	if err != nil {
		return nil, err
	}
	readTimeout, err := toTimeout(settings.ReadTimeout)
	if err != nil {
		return nil, err
	}
	serialSettings := serial.SerialSettings{
		Device:      settings.Port,
		Baudrate:    toBaudrate(settings.Baudrate),
		DataBits:    toDataBits(settings.DataBits),
		StopBits:    stopBits,
		Parity:      parity,
		ReadTimeout: readTimeout,
	}
	locker.Lock()
	defer locker.Unlock()
	s, err := getOrCreateSerial(serialSettings)
	if err != nil {
		return nil, err
	}
	act := Activity{
		settings: settings,
		serial:   s,
	}
	return &act, nil
}

func toStopBits(st float32) (serial.StopBitsType, error) {
	if st == 0 {
		return serial.Stop1, nil
	} else if st == 1 {
		return serial.Stop1, nil
	} else if st == 2 {
		return serial.Stop2, nil
	}
	e := math.Abs(float64(st - 1.5))
	if e < 0.01 {
		return serial.Stop1p5, nil
	} else {
		return serial.Stop1, fmt.Errorf("invalid stop bits: %f", st)
	}
}

var parityMap = map[string]serial.Parity{
	"NONE":  serial.ParityNone,
	"ODD":   serial.ParityOdd,
	"EVEN":  serial.ParityEven,
	"MARK":  serial.ParityMark,
	"SPACE": serial.ParitySpace,
}

func toParity(p string) (serial.Parity, error) {
	trimd := strings.TrimSpace(p)
	if trimd == "" {
		return serial.ParityNone, nil
	}
	pt := strings.ToUpper(trimd)
	if ps, ok := parityMap[pt]; ok {
		return ps, nil
	} else {
		return serial.ParityNone, fmt.Errorf("invalid parity: %s", p)
	}
}

func toDataBits(d int) int {
	if d <= 0 {
		return 8
	}
	return d
}

func toBaudrate(d int) int {
	if d <= 0 {
		return 115200
	}
	return d
}

func toTimeout(str string) (time.Duration, error) {
	s := strings.TrimSpace(str)
	if s == "" {
		return time.Second * 1, nil
	}
	return time.ParseDuration(s)
}

func getOrCreateGlobalLock() (*sync.Mutex, error) {
	name := "serialGlobalLock"
	if s, ok := app.GetValue(name); ok {
		sl, y := s.(*sync.Mutex)
		if y {
			return sl, nil
		} else {
			return nil, errors.New("invalid serial global lock")
		}
	} else {
		s := sync.Mutex{}
		err := app.SetValue(name, &s)
		if err != nil {
			return nil, err
		}
		return &s, nil
	}
}

func getOrCreateSerial(settings serial.SerialSettings) (*serial.Serial, error) {
	key := "serial/" + settings.Device
	s, ok := app.GetValue(key)
	if !ok {
		// create new instance
		sd, err := serial.NewSerial(settings)
		if err != nil {
			return nil, err
		}
		err = app.SetValue(key, sd)
		if err != nil {
			return nil, err
		}
		return sd, nil
	} else {
		switch sd := s.(type) {
		case *serial.Serial:
			{
				return sd, nil
			}
		default:
			{
				return nil, errors.New("not a serial instance")
			}
		}
	}
}

func (a *Activity) Metadata() *activity.Metadata {
	return activityMd
}

func (a *Activity) Eval(ctx activity.Context) (done bool, err error) {
	input := &Input{}
	err = ctx.GetInputObject(input)
	if err != nil {
		return true, err
	}
	if len(input.Data) > 0 {
		n, err := a.serial.Write(input.Data)
		ctx.Logger().Debugf("write data len = %d, err = %+v, data = %+v", n, err, input.Data)

		errStr := ""
		if err != nil {
			errStr = err.Error()
		}
		output := &Output{
			Err:   errStr,
			Count: n,
		}
		err = ctx.SetOutputObject(output)
		return true, err
	} else {
		ctx.Logger().Debug("no data write")
		output := &Output{
			Err:   "",
			Count: 0,
		}
		err = ctx.SetOutputObject(output)
	}
	return true, err
}
